from operator import and_, or_
from django.db.models.query_utils import Q
from django.db.models.sql.constants import LOOKUP_SEP
from django.forms.fields import EMPTY_VALUES
from django.utils.hashcompat import md5_constructor
import pyparsing as pp

_parser_cache = {}

LOOKUP_TYPES = {
    '=' : 'exact', # TODO: Use 'iexact' on strings
    '>' : 'gt',
    '>=': 'gte',
    '<' : 'lt',
    '<=': 'lte'
}

# TODO: Insert docstring
class QueryFilterParser(object):
    def __init__(self, valid_field_names):
        self._expression_stack = []
        self._parser = self._create_parser(valid_field_names)
        
        # Calculate the cache prefix
        ctx = md5_constructor()
        reduce(lambda _, f: ctx.update(f), valid_field_names)
        self._cache_prefix = ctx.hexdigest()
        
    def __call__(self, query_filter):
        return self.parse(query_filter)

    def parse(self, query_filter):
        if query_filter in EMPTY_VALUES:
            return Q() # Filter is empty - don't even bother calling the parser
        
        cache_key = "%s::%s" % (self._cache_prefix, query_filter)
        if cache_key not in _parser_cache: # Check the cache before parsing
            # Always clear the stack before parsing
            self._expression_stack = []
            self._parser(query_filter)
            _parser_cache[cache_key] = self._evaluate_stack()
        return _parser_cache[cache_key]
        
    def _evaluate_stack(self):
        op = self._expression_stack.pop()
        if callable(op): # Operator
            op2 = self._evaluate_stack()
            op1 = self._evaluate_stack()
            return op(op1, op2)
        else: # Q object
            return op
        
    def _create_q_object(self, s, loc, tokens):
        (field_name, lookup_type, field_value) = tokens
        kwdict = {str( "%s%s%s" % (field_name, LOOKUP_SEP, lookup_type) ): field_value}
        self._push( Q(**kwdict) )
    
    def _push(self, object):
        self._expression_stack.append(object)

    def _create_parser(self, valid_field_names):
        field_name_error = "a valid field name (%s)" % (
            ", ".join(valid_field_names)
        )
        field_name = pp.oneOf(valid_field_names).setName(field_name_error)
        
        valid_lookup_types = LOOKUP_TYPES.keys()
        lookup_type_error = "a valid lookup type (%s)" % (
            ", ".join(valid_lookup_types)
        )
        lookup_type = pp.oneOf(valid_lookup_types).setName(
            lookup_type_error).setParseAction(
            lambda s,l,t: [LOOKUP_TYPES[t[0]]]
        )
        
        arith_sign = pp.Word("+-", exact=1)
        int_value = pp.Combine( pp.Optional(arith_sign) + pp.Word(pp.nums) )
        int_value.setName("integer value").setParseAction( lambda s,l,t: [long(t[0])] )

        field_value = int_value | pp.quotedString.setParseAction(pp.removeQuotes)
        field_value.setName("field value")

        and_op = pp.CaselessKeyword("and").setParseAction( lambda s,l,t: [and_] )
        or_op = pp.CaselessKeyword("or").setParseAction( lambda s,l,t: [or_] )
        operators = and_op | or_op

        expression = pp.Forward()
        term = pp.Group(
            (field_name + lookup_type + field_value).setParseAction(self._create_q_object) |
            ( pp.Suppress("(") + expression.suppress() + pp.Suppress(")") )
        )
                
        expression << term + pp.ZeroOrMore(
            (operators + term).setParseAction( lambda s,l,t: [self._push(t[0])] )
        )

        # Parser requires that the entire input string be successfully parsed   
        return lambda x: expression.parseString(x, parseAll=True)
